/*
 * Copyright (C) 2010 Fores Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package jp.fores.midori.server.plugin.impl;

import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletConfig;

import jp.fores.midori.core.dto.RPCInputDto;
import jp.fores.midori.core.dto.RPCOutputDto;
import jp.fores.midori.core.util.ClassUtil;
import jp.fores.midori.server.WebContext;
import jp.fores.midori.server.exception.IllegalServiceClassException;
import jp.fores.midori.server.exception.RPCTargetMethodNotFoundException;
import jp.fores.midori.server.exception.RPCTargetServiceNotFoundException;
import jp.fores.midori.server.plugin.RPCPlugin;
import jp.fores.midori.server.plugin.RPCPluginUtil;
import jp.fores.midori.server.plugin.RPCTargetMethodManager;

import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.thrift.TProcessor;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.protocol.TProtocolFactory;
import org.apache.thrift.transport.TIOStreamTransport;
import org.apache.thrift.transport.TTransport;
import org.apache.thrift.transport.TTransportFactory;

/**
 * Thrift形式のリクエスト・レスポンスの処理を行うプラグインクラス。<br>
 */
public class ThriftPlugin implements RPCPlugin {
	//==========================================================
	//定数

	/**
	 * ログ出力用
	 */
	private static final Log log = LogFactory.getLog(ThriftPlugin.class);

	/**
	 * Content-Type
	 */
	public static final String CONTENT_TYPE = "application/x-thrift";


	//==========================================================
	//フィールド

	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//クラスフィールド
	//(少しでも処理効率を上げるため、プラグインクラスのどのインスタンスでも共通して使用したいので、あえてstaticなクラスフィールドにしている)

	/**
	 * 入力用のトランスポートファクトリー
	 */
	protected static TTransportFactory inputTransportFactory = new TTransportFactory();

	/**
	 * 出力用のトランスポートファクトリー
	 */
	protected static TTransportFactory outputTransportFactory = new TTransportFactory();

	/**
	 * 入力用のプロトコルファクトリー
	 */
	protected static TProtocolFactory inputProtocolFactory = new TBinaryProtocol.Factory();

	/**
	 * 出力用のプロトコルファクトリー
	 */
	protected static TProtocolFactory outputProtocolFactory = new TBinaryProtocol.Factory();


	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//インスタンスフィールド

	/**
	 * プロセッサクラスのコンストラクタ情報をキャッシュするMap
	 */
	//(キーはサービスのクラス名)
	protected final Map<String, Constructor<TProcessor>> constructorCache = new HashMap<String, Constructor<TProcessor>>();


	//==========================================================
	//メソッド

	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//抽象メソッドの実装

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void handleRequest(WebContext context) throws Exception {
		//RPCの出力データを格納するためのDTOクラスのインスタンスを生成
		RPCOutputDto outputDto = new RPCOutputDto();

		//リクエストの内容を元にして、呼び出し対象のサービスクラスの完全指定のクラス名を取得
		String className = RPCPluginUtil.getTargetClassName(context.request);

		//デバッグログが有効な場合
		if (log.isDebugEnabled()) {
			//デバッグログ出力
			log.debug("呼び出し対象のクラス名:" + className);
		}

		//クラス名が取得できなかった場合
		if ((className == null) || "".equals(className)) {
			//RPCの対象のサービスクラスが見つからなかった場合の例外を投げる
			throw new RPCTargetServiceNotFoundException("呼び出し対象のクラス名を取得できませんでした。");
		}

		//コンテナから呼び出し対象のサービスクラスのインスタンスを取得
		Object service = RPCPluginUtil.getService(context.container, className);

		//レスポンスのContent-Typeに定数の値を設定する
		context.response.setContentType(CONTENT_TYPE);

		//サービスクラスのクラスオブジェクトから、プロセッサクラスのコンストラクタを取得
		Constructor constructor = getConstructor(service.getClass());

		//プロセッサクラスのコンストラクタを使って、インスタンスを生成
		//(引数にはサービスクラスのインスタンスを指定する)
		TProcessor processor = (TProcessor) constructor.newInstance(service);

		//入出力用のトランスポート
		TTransport inputTransport = null;
		TTransport outputTransport = null;

		//入出力ストリームの取得
		InputStream in = context.request.getInputStream();
		OutputStream out = context.response.getOutputStream();

		try {
			//入出力ストリームを元にして、トランスポートクラスのインスタンスを生成する
			TTransport transport = new TIOStreamTransport(in, out);

			//ファクトリークラスを使って、入出力用のプロトコルクラスのインスタンスを生成
			inputTransport = inputTransportFactory.getTransport(transport);
			outputTransport = outputTransportFactory.getTransport(transport);
			TProtocol inputProtocol = inputProtocolFactory.getProtocol(inputTransport);
			TProtocol outputProtocol = outputProtocolFactory.getProtocol(outputTransport);

			//TProcessorクラスと入出力用のプロトコルクラスを使って、実際の処理を呼び出す
			processor.process(inputProtocol, outputProtocol);
		}
		//終了処理
		finally {
			//入出力ストリームを確実に閉じる
			IOUtils.closeQuietly(in);
			IOUtils.closeQuietly(out);

			//入力用のトランスポートがnullでない場合
			if (inputTransport != null) {
				try {
					//入力用のトランスポートを閉じる
					inputTransport.close();
				}
				//例外処理
				//(どうしようもないので、ログ出力をするだけでエラー扱いはしない)
				catch (Exception e) {
					//デバッグログが有効な場合
					if (log.isDebugEnabled()) {
						//デバッグログ出力
						log.debug("入力用のトランスポートを閉じるのに失敗しました。", e);
					}
				}
			}

			//出力用のトランスポートがnullでない場合
			if (outputTransport != null) {
				try {
					//出力用のトランスポートを閉じる
					outputTransport.close();
				}
				//例外処理
				//(どうしようもないので、ログ出力をするだけでエラー扱いはしない)
				catch (Exception e) {
					//デバッグログが有効な場合
					if (log.isDebugEnabled()) {
						//デバッグログ出力
						log.debug("出力用のトランスポートを閉じるのに失敗しました。", e);
					}
				}
			}
		}
	}

	/**
	 * 初期化処理を行います。<br>
	 * この処理はサーブレットのinit()メソッドのタイミングで呼ばれます。<br>
	 * このクラスの実装では何もしませんので、必要に応じてサブクラスでこのメソッドをオーバーライドして下さい。<br>
	 *
	 * @param config サーブレットコンフィグ
	 * @throws Exception 例外
	 */
	@Override
	public void init(ServletConfig config) throws Exception {
		//特に何も行わない
	}

	/**
	 * 終了処理を行います。<br>
	 * この処理はサーブレットのdestroy()メソッドのタイミングで呼ばれます。<br>
	 * このクラスの実装では何もしませんので、必要に応じてサブクラスでこのメソッドをオーバーライドして下さい。<br>
	 *
	 * @throws Exception 例外
	 */
	@Override
	public void destroy() throws Exception {
		//特に何も行わない
	}


	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//内部処理用
	//(サブクラスで機能拡張が自由に行えるようにするため、全てprotectedで宣言しておきます)

	/**
	 * サービスクラスのインスタンスから、RPCの入力データを格納するためのDTOの内容に適合するRPC対象のメソッドを取得します。<br>
	 * 指定されたオブジェクトがRPCの対象クラスとしてふさわしくない場合や、適合するメソッドが存在しない場合は例外を投げます。<br>
	 *
	 * @param service サービスクラスのインスタンス
	 * @param inputDto RPCの入力データを格納するためのDTOクラスのインスタンス
	 * @return 適合するメソッド
	 * @throws IllegalServiceClassException 指定されたオブジェクトがRPCの対象クラスとしてふさわしくない場合
	 * @throws RPCTargetMethodNotFoundException 適合するメソッドが存在しない場合
	 * @throws Exception 例外
	 */
	protected Method findRPCTargetMethod(Object service, RPCInputDto inputDto)
			throws IllegalServiceClassException,
			RPCTargetMethodNotFoundException, Exception {
		//RPCの対象のメソッド情報を管理するクラスに処理を移譲する
		return RPCTargetMethodManager.getInstance().findRPCTargetMethod(service, inputDto.methodName, inputDto.args);
	}

	/**
	 * サービスクラスのクラスオブジェクトから、プロセッサクラスのコンストラクタを取得します。<br>
	 *
	 * @param clazz サービスクラスのクラスオブジェクト
	 * @return プロセッサクラスのコンストラクタ
	 * @throws Exception 例外
	 */
	protected Constructor<TProcessor> getConstructor(Class clazz)
			throws Exception {
		//クラスオブジェクトからクラス名を取得
		String className = clazz.getName();

		//プロセッサクラスのコンストラクタ情報をキャッシュするMapから、クラス名をキーにしてコンストラクタを取得する
		Constructor<TProcessor> constructor = this.constructorCache.get(className);

		//キャッシュから取得できなかった場合
		if (constructor == null) {
			//==========================================================
			//インターフェースクラスを取得

			//インターフェースクラス
			Class ifClass = null;

			//サービスクラスが実装しているインターフェースに対してループをまわす
			for (Class c : clazz.getInterfaces()) {
				//クラス名に「$Iface」が含まれている場合
				//(「$」は内部クラスで自動的につけられる文字)
				if (c.getName().indexOf("$Iface") != -1) {
					//現在のクラスオブジェクトをインターフェースクラスとして採用する
					ifClass = c;

					//目的のクラスオブジェクトが見つかったので、ループを抜ける
					break;
				}
			}

			//インターフェースクラスが見つからなかった場合
			if (ifClass == null) {
				//サービスクラスが不正な場合の例外を投げる
				throw new IllegalServiceClassException("Thrift用のサービスサラスではありません。");
			}


			//==========================================================
			//プロセッサクラスを取得

			//インターフェースクラスのクラス名を取得
			String ifClassName = ifClass.getName();

			//インターフェースクラスのクラス名を元にして、プロセッサクラスのクラス名を作成
			//(「$」以降の部分を、「$Processor」に差し替える)
			String processClassName = ifClassName.substring(0, ifClassName.lastIndexOf("$"))
					+ "$Processor";

			//プロセッサクラスのクラス名に対応するクラスオブジェクトをキャッシュ付きで取得する
			Class<TProcessor> processorClass = (Class<TProcessor>) ClassUtil.forNameWithCache(processClassName);


			//==========================================================
			//プロセッサクラスのコンストラクタを取得

			//プロセッサクラスのコンストラクタを取得
			//(引数にインターフェースクラスをとるものを取得する)
			constructor = processorClass.getConstructor(ifClass);

			//作成したコンストラクタをフィールドのキャッシュ用のMapに登録する
			//(synchronizedで同期をとっていないので、複数スレッドで同時にこの処理を行った場合、
			// キャッシュのMapに登録されるオブジェクトがスレッド毎に異なる可能性があるが、
			// プロセッサクラスのコンストラクタは毎回同じオブジェクトになるので問題ない)
			this.constructorCache.put(className, constructor);
		}

		//キャッシュから取得、または作成したコンストラクタを返す
		return constructor;
	}
}
